package com.suncky.frame.utils.image;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.AsyncTask;

import androidx.annotation.NonNull;

import com.suncky.frame.AppConfig;
import com.suncky.frame.utils.LogUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 图片压缩工具
 */
public class ImageCompressor {
    private final Context context;
    private String targetDir;
    private int ignoreSize = 0;//压缩到的大小(kb)
//    private int diffValue = 10;//过压缩后的最终大小和阈值的差值范围(kb)
    private int toHeight = 0;//输出高度
    private int toWidth = 0;//输出宽度
    private boolean uniformScale = true;//是否等比例缩放到输出宽高
    private Bitmap.CompressFormat outputFormat = null;//输出格式
    private CompressCallback callback;
    private final List<InputStreamProvider> inputStreamProviders;
    private final MainThreadExecutor mainThreadExecutor;
    private boolean isError = false;//是否出现异常
    private boolean isCanceled = false;//压缩操作是否已被取消

    private ImageCompressor(Context context) {
        this.context = context;
        inputStreamProviders = new ArrayList<>();
        mainThreadExecutor = new MainThreadExecutor(context);
    }

    public static ImageCompressor with(Context context) {
        return new ImageCompressor(context);
    }

    /**
     * 原图片路径
     * @param sourcePaths 路径数组
     * @return
     */
    public ImageCompressor source(@NonNull String... sourcePaths) {
        for (String path : sourcePaths) {
            this.inputStreamProviders.add(new InputStreamProvider() {
                @Override
                public InputStream getInputStream() throws IOException {
                    return new FileInputStream(path);
                }

                @Override
                public String getPath() {
                    return path;
                }
            });
        }
        return this;
    }

    /**
     * 原图片路径
     * @param sourceUris 路径数组
     * @return
     */
    public ImageCompressor source(@NonNull Uri... sourceUris) {
        for (Uri uri : sourceUris) {
            this.inputStreamProviders.add(new InputStreamProvider() {
                @Override
                public InputStream getInputStream() throws IOException {
                    return context.getContentResolver().openInputStream(uri);
                }

                @Override
                public String getPath() {
                    return uri.getPath();
                }
            });
        }
        return this;
    }

    /**
     * 原图片路径
     * @param sourceUFiles 路径数组
     * @return
     */
    public ImageCompressor source(@NonNull File... sourceUFiles) {
        for (File file : sourceUFiles) {
            this.inputStreamProviders.add(new InputStreamProvider() {
                @Override
                public InputStream getInputStream() throws IOException {
                    return new FileInputStream(file);
                }

                @Override
                public String getPath() {
                    return file.getAbsolutePath();
                }
            });
        }
        return this;
    }

    /**
     * 原图片路径
     * @param sources 路径数组
     * @return
     */
    public <T> ImageCompressor source(@NonNull List<T> sources) {
        for (T src : sources) {
            if (src instanceof String) {
                source((String) src);
            } else if (src instanceof File) {
                source((File) src);
            } else if (src instanceof Uri) {
                source((Uri) src);
            } else {
                throw new IllegalArgumentException("Original data type exception, it must be String, File, Uri");
            }
        }
        return this;
    }

    /**
     * 压缩图片输出目录
     * @param targetDir 输出目录
     * @return
     */
    public ImageCompressor targetDir(String targetDir) {
        this.targetDir = targetDir;
        return this;
    }

    /**
     * 图片输出最大尺寸,当原图片的宽或高超过最大值时,压缩到最大值。输出宽高均不大于0 或者 均大于原始宽高时 不压缩尺寸.
     * 如果经过尺寸压缩后,图片质量大小仍然大于已设定的阈值(ignoreSize),将继续进行压缩(png图片继续压缩尺寸(等比例),其他图片压缩质量)
     *
     * @param width  最大宽度
     * @param height 最大高度
     * @param uniformScale 设为true时,等比例缩放尺寸,此时输出的尺寸不一定和设置的尺寸完全一致.设为false时,完全按照设置的尺寸进行缩放,此输出的图片可能会发生变形.
     * @return
     */
    public ImageCompressor maxWidthHeight(int width, int height, boolean uniformScale) {
        this.toWidth = width;
        this.toHeight = height;
        this.uniformScale = uniformScale;
        return this;
    }

    /**
     * 图片压缩的阈值(kb).
     * 如果原图片的大小大于阈值时,压缩到阈值以下.原图片的大小不超过阈值时不进行压缩;
     * 不设置阈值或者阈值不大于0时按默认质量进行压缩.
     *
     * @param maxSize 阈值(kb)
     * @return
     */
    public ImageCompressor maxSize(int maxSize) {
        this.ignoreSize = maxSize;
        return this;
    }

    /**
     * 图片压缩的阈值(kb).
     * 如果原图片的大小大于阈值时,压缩到阈值以下.原图片的大小不超过阈值时不进行压缩;
     * 不设置阈值或者阈值不大于0时按默认质量进行压缩.
     *
     * @param ignoreSize 阈值(kb)
     * @param diffValue  差值：进过压缩后的最终大小和阈值的差值范围(kb)。最小0kb，最大不超过阈值，默认10kb。
     *                   当阈值不大于0或者原图片大小小于阈值时，此参数无效。
     *                   (注意：差值范围越小，输出图片大小越接近阈值，但是压缩效率会降低)
     * @return
     */
//    public ImageCompressor ignoreSize(int ignoreSize, int diffValue) {
//        this.ignoreSize = ignoreSize;
//        this.diffValue = Math.max(diffValue, 0);
//        this.diffValue = Math.min(this.diffValue, ignoreSize);
//        return this;
//    }

    /**
     * 图片输出格式. 默认按原图片格式输出
     * @param outputFormat 输出格式
     * @return
     */
    public ImageCompressor outputFormat(Bitmap.CompressFormat outputFormat) {
        this.outputFormat = outputFormat;
        return this;
    }

    /**
     * 设置回调
     * @param callback
     * @return
     */
    public ImageCompressor compressCallback(CompressCallback callback) {
        this.callback = callback;
        return this;
    }

    /**
     * 取消压缩
     */
    public void cancel() {
        this.isCanceled = true;
    }

    /**
     * 是否被取消
     * @return
     */
    public boolean isCanceled() {
        return isCanceled;
    }

    /**
     * 开始压缩图片
     */
    public void launch() {
        if (inputStreamProviders.size() == 0) {
            return;
        }
        File[] toFiles = new File[this.inputStreamProviders.size()];
        if (callback != null) {
            callback.onStart();
        }
        LogUtils.i("=============================================================================================");
        LogUtils.i("compress start! ignore size is " + ignoreSize + " kb; max width is " + toWidth + "; max height is " + toHeight);
        LogUtils.i("--------------------------------------------");
        long startMillis = System.currentTimeMillis();
        for (int i = 0; i < inputStreamProviders.size(); ++i) {
            int index = i;
            AsyncTask.SERIAL_EXECUTOR.execute(() -> {
                if (isError || isCanceled) {
                    return;
                }
                try {
                    File result = compress(index);
                    toFiles[index] = result;
                    if (!isError && !isCanceled && index == toFiles.length - 1 && callback != null) {
                        LogUtils.i("compress finish! takes " + (System.currentTimeMillis() - startMillis) + " milliseconds");
                        LogUtils.i("=============================================================================================");
                        mainThreadExecutor.execute(() -> {
                            callback.onSuccess(toFiles);
                        });
                    }
                } catch (Exception e) {
                    isError = true;
                    if (callback != null) {
                        mainThreadExecutor.execute(()->{
                            callback.onError(e);
                        });
                    }
                }
            });
        }
    }

    private File compress(int pathIndex) throws Exception {
        Bitmap srcBitmap;
        String mimeType = getMIMEType(pathIndex);
        InputStream is = inputStreamProviders.get(pathIndex).getInputStream();
        int originalSize = is.available() / 1024;
        LogUtils.i("image " + pathIndex + " original size: " + originalSize + "kb");
        if ("image/jpeg".equals(mimeType)) {
            srcBitmap = rotateBitmap(is, pathIndex);
        } else {
            srcBitmap = BitmapFactory.decodeStream(is);
        }
        is.close();
        if (outputFormat == null) {
            if ("image/png".equals(mimeType)) {
                outputFormat = Bitmap.CompressFormat.PNG;
            } else {
                outputFormat = Bitmap.CompressFormat.JPEG;
            }
        }
        String postfix = ".jpg";
        if (outputFormat == Bitmap.CompressFormat.PNG) {
            postfix = ".png";
        } else if (outputFormat == Bitmap.CompressFormat.WEBP) {
            postfix = ".webp";
        }
        if (targetDir == null || targetDir.length() == 0) {
            targetDir = AppConfig.imagePath;
        }
        File toDirFile = new File(targetDir);
        if (!toDirFile.exists()) {
            toDirFile.mkdirs();
        }
        String toPath = targetDir + File.separator + "aframe_compress_" + System.currentTimeMillis() + postfix;
        boolean needScale = needScale(srcBitmap);
        if (originalSize <= ignoreSize && !needScale) {
            srcBitmap.recycle();
            LogUtils.i("--------------------------------------------");
            Utils.writeFile(inputStreamProviders.get(pathIndex).getInputStream(), toPath);
            return new File(toPath);
        }
        if (needScale) {
            //根据给定的最大尺寸缩放图片
            srcBitmap = Utils.scaleBitmap(srcBitmap, toWidth, toHeight, uniformScale);
        }
        ByteArrayOutputStream baos = compressBitmap(srcBitmap, originalSize, needScale);
        LogUtils.i("image " + pathIndex + " output size: " + baos.size() / 1024 + "kb");

        File toFile = new File(toPath);
        FileOutputStream out = new FileOutputStream(toFile);
        out.write(baos.toByteArray());
        out.flush();
        out.close();
        baos.close();
        LogUtils.i("--------------------------------------------");
        srcBitmap.recycle();
        return toFile;
    }

    //纠正图片角度
    private Bitmap rotateBitmap(InputStream is, int pathIndex) throws IOException {
        int degree;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            InputStream isAngle = inputStreamProviders.get(pathIndex).getInputStream();
            degree = Utils.getAngle(isAngle);
            isAngle.close();
        } else {
            degree = Utils.getAngle(inputStreamProviders.get(pathIndex).getPath());
        }
        Bitmap srcBitmap = BitmapFactory.decodeStream(is);
        if (degree != 0) {
            srcBitmap = Utils.getRotateBitmap(srcBitmap, degree);
        }
        return srcBitmap;
    }

    //获取图片MIME类型
    private String getMIMEType(int pathIndex) throws IOException {
        InputStream is = inputStreamProviders.get(pathIndex).getInputStream();
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is,null, options);
        is.close();
        return options.outMimeType;
    }

    private boolean needScale(Bitmap srcBitmap) {
        return (toWidth > 0 && toWidth < srcBitmap.getWidth()) || (toHeight > 0 && toHeight < srcBitmap.getHeight());
    }

    /**
     * 压缩图片
     * @param srcBitmap 压缩的bitmap
     * @param originalSize 原图片大小(kb)
     * @param scaled 是否已经过缩放处理
     * @return
     */
    private ByteArrayOutputStream compressBitmap(Bitmap srcBitmap, int originalSize, boolean scaled) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        if (ignoreSize <= 0) {
            if (!scaled) {
                //未进行尺寸压缩时,按默认值压缩
                if (outputFormat != Bitmap.CompressFormat.PNG) {
                    srcBitmap.compress(outputFormat, 60, baos);
                } else {
                    Matrix matrix = new Matrix();
                    matrix.setScale(0.7f, 0.7f);
                    Bitmap resultBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight(), matrix, true);
                    resultBitmap.compress(outputFormat, 100, baos);
                    resultBitmap.recycle();
                }
            } else {
                //进行过尺寸压缩,填充baos
                srcBitmap.compress(outputFormat, 100, baos);
            }
            if (baos.size() / 1024 > originalSize) {
                //如果压缩后的大小比原图片还大,继续压缩到原图片大小30%以下
                compressTo(srcBitmap, baos, (originalSize * 30 / 100));
            }
        } else {
            compressTo(srcBitmap, baos, Math.min(originalSize, ignoreSize));
        }
        srcBitmap.recycle();
        return baos;
    }

    /**
     * 图片压缩到最大大小以下
     * @param srcBitmap
     * @param baos
     * @param maxSize
     * @return
     */
    private void compressTo(Bitmap srcBitmap, ByteArrayOutputStream baos, int maxSize) {
        if (outputFormat != Bitmap.CompressFormat.PNG) {//非PNG图片可进行质量和尺寸压缩
            compressQuality(srcBitmap, baos, maxSize);
        } else {//png图片只压缩尺寸
            compressSize(srcBitmap, baos, maxSize);
        }
    }

    //压缩质量
    private int compressQuality(Bitmap srcBitmap, ByteArrayOutputStream baos, int maxSize) {
        baos.reset();
        srcBitmap.compress(outputFormat, 0, baos);
        int restKb = baos.size() / 1024;
        //如果最低质量压缩后仍不小于maxSize,返回
        if (restKb >= maxSize) {
            return restKb;
        }
        baos.reset();
        srcBitmap.compress(outputFormat, 100, baos);
        restKb = baos.size() / 1024;
        //如果最高质量压缩后仍不大于maxSize,返回
        if (restKb <= ignoreSize) {
            return restKb;
        }
        int diffValue = maxSize / 10;//压缩后的大小与maxSize的差值
        int lowQuality = 0;
        int highQuality = 100;
        int midQuality;
        while (!isCanceled) {
            midQuality = (lowQuality + highQuality) / 2;
            // 清空baos
            baos.reset();
            srcBitmap.compress(outputFormat, midQuality, baos);
            restKb = baos.size() / 1024;

            if (highQuality - lowQuality < 2) {
                if (maxSize - restKb > diffValue) {
                    //压缩结果大小差值不符合要求,回到上次压缩状态
                    baos.reset();
                    srcBitmap.compress(outputFormat, highQuality, baos);
                    restKb = baos.size() / 1024;
                }
                break;
            }
            if (restKb > maxSize) {
                highQuality = midQuality;
            } else if (maxSize - restKb > diffValue) {
                lowQuality = midQuality;
            } else {
                break;
            }
        }
        if (restKb > maxSize) {
            //如果经过质量压缩后未达到目标,继续进行尺寸压缩
            srcBitmap.recycle();
            srcBitmap = BitmapFactory.decodeByteArray(baos.toByteArray(), 0, baos.size());
            restKb = compressSize(srcBitmap, baos, maxSize);
        }
        return restKb;
    }

    //压缩尺寸
    private int compressSize(Bitmap srcBitmap, ByteArrayOutputStream baos, int maxSize) {
        Matrix matrix = new Matrix();
        float lowScale = 0f;
        float highScale = 1f;
        float midScale;
        int diffValue = maxSize / 10;//压缩后的大小与maxSize的差值
        int restKb = 0;
        while (!isCanceled) {
            midScale = (lowScale + highScale) / 2;
            matrix.setScale(midScale, midScale);
            Bitmap resultBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight(), matrix, true);
            baos.reset();
            resultBitmap.compress(outputFormat, 100, baos);
            resultBitmap.recycle();
            restKb = baos.size() / 1024;

            if (highScale - lowScale < 0.000002) {
                break;
            }
            if (restKb > maxSize) {
                highScale = midScale;
            } else if (maxSize - restKb > diffValue) {
                lowScale = midScale;
            } else {
                break;
            }

        }
        return restKb;
    }
    public interface CompressCallback{
        void onStart();

        void onSuccess(File... results);

        void onError(Throwable throwable);
    }
}
